//
//  HHAudioPlayer.m
//  Pod
//
//  Created by chun.chen on 2018/4/21.
//  Copyright © 2018年 Zhihan. All rights reserved.
//
//

#import "HHAudioPlayer.h"
#import <AVFoundation/AVFoundation.h>
#import <BCFoundation/NSString+BCHelper.h>
#import <BCFoundation/BCFoundationUtils.h>
#import <objc/runtime.h>

NSString *const kETAAudioDataKey;

@interface HHAudioPlayer () <AVAudioPlayerDelegate>
{
    // 型号监测时间队列
    dispatch_source_t _signalTimer;
}

@property (nonatomic, copy, readonly) NSString *cachePath;//语音缓存路径,以URLString的MD5编码为key保存
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, strong) NSOperationQueue *audioDataOperationQueue;//下载音频地址队列
@property (nonatomic, strong) NSHashTable *bindedDelegates;
@property (nonatomic, strong) NSTimer *audioTimer;
@end

@implementation HHAudioPlayer
@synthesize cachePath = _cachePath;

#pragma mark - system
+ (void)initialize {
    //配置播放器配置
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
//    [[AVAudioSession sharedInstance] setActive:YES error:nil];
}

+ (instancetype)sharePlayer {
    static dispatch_once_t onceToken;
    static id shareInstance;
    dispatch_once(&onceToken, ^{
        shareInstance = [[self alloc] init];
    });
    return shareInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        _bindedDelegates = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory capacity:0];

        //添加应用进入后台通知
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(applicationWillResignActive:)
                                                     name:UIApplicationWillResignActiveNotification
                                                   object:[UIApplication sharedApplication]];
        // 添加声音被打断处理
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterruption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:[AVAudioSession sharedInstance]];
        _signalTime = 0.2;
    }
    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[UIDevice currentDevice] setProximityMonitoringEnabled:NO];
}

#pragma mark - bind unbind
- (void)bind:(id<HHAudioPlayerDelegate>)delegate
{
    if (!delegate) {
        return;
    }
    if (![_bindedDelegates containsObject:delegate]) {
        [_bindedDelegates addObject:delegate];
    }
}

- (void)unbind:(id<HHAudioPlayerDelegate>)delegate
{
    [_bindedDelegates removeObject:delegate];
}

#pragma mark - getter
/**
 *  lazy load audioDataOperationQueue
 *
 *  @return NSOperationQueue
 */
- (NSOperationQueue *)audioDataOperationQueue {
    if (_audioDataOperationQueue == nil) {
        NSOperationQueue *audioDataOperationQueue = [[NSOperationQueue alloc] init];
        audioDataOperationQueue.name = @"com.huihui.AVAudipPlayer.loadAudioDataQueue";
        _audioDataOperationQueue = audioDataOperationQueue;
    }
    return _audioDataOperationQueue;
}

#pragma mark - Public Methods
- (void)playAudioWithURLString:(NSString *)URLString identifier:(NSString *)identifier {
    if (!URLString) {
        NSError *error = [NSError errorWithDomain:@"HHAudioPlayer" code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"URLString is nil" }];
        [self notifyBeganWirhError:error];
        return;
    }
    if (!identifier) {
        identifier = @"";
    }

    //如果来自同一个URLString并且index相同,则直接取消
    if ([self.URLString isEqualToString:URLString] && self.identifier == identifier) {
        [self stopAudioPlayer];
        [self setAudioPlayerState:HHAudioPlayState_Cancel];
        return;
    }

    //TODO 从URL中读取音频data
    self.URLString = URLString;
    self.identifier = identifier;

    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_main_async_bcsafe(^{
                                       [self setAudioPlayerState:HHAudioPlayState_Downloading];
                                   });
        NSData *audioData = [self audioDataFromURLString:URLString identifier:identifier];
        if (!audioData) {
            dispatch_main_async_bcsafe(^{
                                           [self setAudioPlayerState:HHAudioPlayState_Cancel];
                                       });
            return;
        }
        dispatch_main_async_bcsafe(^{
                                       [self playAudioWithData:audioData];
                                   });
    }];

    [blockOperation setName:[[NSString stringWithFormat:@"%@_%@", self.URLString, self.identifier] bc_md5]];

    [self.audioDataOperationQueue addOperation:blockOperation];
}

- (void)cleanAudioCache {
    NSArray *files = [[NSFileManager defaultManager] subpathsAtPath:self.cachePath];
    for (NSString *file in files) {
        [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    }
}

- (void)stopAudioPlayer {
    [self setAudioPlayerState:HHAudioPlayState_Cancel];
    if (_audioPlayer) {
        _audioPlayer.playing ? [_audioPlayer stop] : nil;
        _audioPlayer.delegate = nil;
        _audioPlayer = nil;
    }
    [self stopMonitorPlayerSingnal];
}

- (void)resetAudioPlayer {
    [self stopAudioPlayer];
    _URLString = nil;
    _identifier = nil;
}

- (void)pause {
    [_audioPlayer pause];
    [self setAudioPlayerState:HHAudioPlayState_Pause];
    [self stopMonitorPlayerSingnal];
}

- (void)resumePlay {
    [_audioPlayer play];
    [self setAudioPlayerState:HHAudioPlayState_Playing];
    [self startMonitorPlayerSingnal];
}

- (BOOL)playAtTime:(NSTimeInterval)time {
    [self setAudioPlayerState:HHAudioPlayState_Playing];
    [self.audioPlayer setCurrentTime:time];
    [self.audioPlayer play];
    [self startMonitorPlayerSingnal];
    return YES;
}

- (void)setSignalTime:(CGFloat)signalTime {
    _signalTime = signalTime;
}

/**
 开始监听声音信号量
 */
- (void)startMonitorPlayerSingnal {
    if (!self.audioPlayer) {
        return;
    }
    _signalTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(_signalTimer, DISPATCH_TIME_NOW, _signalTime * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);

    @BCWeakify(self);
    dispatch_source_set_event_handler(_signalTimer, ^{
        @BCStrongify(self);
        [self.audioPlayer updateMeters];
        double lowPassResults = pow(10, (0.05 * [self.audioPlayer peakPowerForChannel:0]));
        NSHashTable *bindDelegates = [self.bindedDelegates mutableCopy];
        for (id<HHAudioPlayerDelegate> bindItem in bindDelegates) {
            if ([bindItem respondsToSelector:@selector(audioPlayerSignalPowerDidChange:)]) {
                [bindItem audioPlayerSignalPowerDidChange:lowPassResults];
            }
        }
    });
    dispatch_resume(_signalTimer);
}

/**
 停止监听声音信号量
 */
- (void)stopMonitorPlayerSingnal {
    if (_signalTimer) {
        dispatch_source_cancel(_signalTimer);
        _signalTimer = nil;
    }
}

#pragma mark - Private Methods

- (NSData *)audioDataFromURLString:(NSString *)URLString identifier:(NSString *)identifier {
    NSData *audioData;

    //1.检查URLString是本地文件还是网络文件
    if ([URLString hasPrefix:@"http"] || [URLString hasPrefix:@"https"]) {
        //2.来自网络,先检查本地缓存,缓存key是URLString的MD5编码
        NSString *audioCacheKey = [URLString bc_md5];

        //3.本地缓存存在->直接读取本地缓存   不存在->从网络获取数据,并且缓存
        if ([[NSFileManager defaultManager] fileExistsAtPath:[self.cachePath stringByAppendingPathComponent:audioCacheKey]]) {
            audioData = [NSData dataWithContentsOfFile:[self.cachePath stringByAppendingPathComponent:audioCacheKey]];
        } else {
            audioData = [NSData dataWithContentsOfURL:[NSURL URLWithString:URLString]];
            [audioData writeToFile:[self.cachePath stringByAppendingPathComponent:audioCacheKey] atomically:YES];
        }
    } else {
        audioData = [NSData dataWithContentsOfFile:URLString];
    }
    //4.判断audioData是否读取成功,成功则添加对应的audioDataKey
    if (audioData) {
        objc_setAssociatedObject(audioData, &kETAAudioDataKey, [[NSString stringWithFormat:@"%@_%@", URLString, identifier] bc_md5], OBJC_ASSOCIATION_COPY);
    }

    return audioData;
}

- (void)playAudioWithData:(NSData *)audioData {
    NSString *audioURLMD5String = objc_getAssociatedObject(audioData, &kETAAudioDataKey);

    if (![[[NSString stringWithFormat:@"%@_%@", self.URLString, self.identifier] bc_md5] isEqualToString:audioURLMD5String]) {
        return;
    }

    NSError *audioPlayerError;
    HHAudioMediaType mediaType = [self mediaTypeWithUrl:self.URLString];
    if (mediaType == HHAudioMediaType_unknow) {
        _audioPlayer = [[AVAudioPlayer alloc] initWithData:audioData error:&audioPlayerError];
    } else {
        // 使用指定类型
        NSString *utiString = AVFileTypeCoreAudioFormat;
        if (mediaType == HHAudioMediaType_mp3) {
            utiString = AVFileTypeMPEGLayer3;
        } else if (mediaType == HHAudioMediaType_caf) {
            utiString = AVFileTypeCoreAudioFormat;
        } else if (mediaType == HHAudioMediaType_wav) {
            utiString = AVFileTypeWAVE;
        } else if (mediaType == HHAudioMediaType_amr) {
            utiString = AVFileTypeAMR;
        }
        _audioPlayer =  [[AVAudioPlayer alloc] initWithData:audioData fileTypeHint:utiString error:&audioPlayerError];
    }

    if (audioPlayerError || !_audioPlayer || !audioData) {
        [self setAudioPlayerState:HHAudioPlayState_Cancel];
        [self notifyBeganWirhError:audioPlayerError];
        return;
    }

    _audioPlayer.volume = 1.0f;
    _audioPlayer.delegate = self;
    [_audioPlayer prepareToPlay];
    [self setAudioPlayerState:HHAudioPlayState_Playing];
    [_audioPlayer play];
    [_audioPlayer setMeteringEnabled:YES];
    if (self.isOpenSignal) {
        [self startMonitorPlayerSingnal];
    }
    [self notifyBeganWirhError:nil];
}

/// 广播开始播放
/// @param error error description
- (void)notifyBeganWirhError:(NSError *)error {
    NSHashTable *bindDelegates = [self.bindedDelegates mutableCopy];
    for (id<HHAudioPlayerDelegate> bindItem in bindDelegates) {
        if ([bindItem respondsToSelector:@selector(playAudio:didBeganWithError:)]) {
            [bindItem playAudio:self didBeganWithError:error];
        }
    }
}

- (void)cancelOperation {
    for (NSOperation *operation in self.audioDataOperationQueue.operations) {
        if ([operation.name isEqualToString:[[NSString stringWithFormat:@"%@_%@", self.URLString, self.identifier] bc_md5]]) {
            [operation cancel];
            break;
        }
    }
}

#pragma mark - timer
- (void)updatePlayerDuration {
    NSHashTable *bindDelegates = [self.bindedDelegates mutableCopy];
    for (id<HHAudioPlayerDelegate> bindItem in bindDelegates) {
        if ([bindItem respondsToSelector:@selector(audioPlayerUpdateDuration:)]) {
            [bindItem audioPlayerUpdateDuration:self.audioPlayer.currentTime];
        }
    }
}

#pragma mark - AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
    [self setAudioPlayerState:EHHAudioPlayState_End];

    NSHashTable *bindDelegates = [self.bindedDelegates mutableCopy];
    for (id<HHAudioPlayerDelegate> bindItem in bindDelegates) {
        if ([bindItem respondsToSelector:@selector(audioPlayerDidFinishPlaying:successfully:)]) {
            [bindItem audioPlayerDidFinishPlaying:self successfully:flag];
        }
    }
    [self stopMonitorPlayerSingnal];
    //    //删除近距离事件监听
    //    [[UIDevice currentDevice] setProximityMonitoringEnabled:NO];
    //    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil];
    //延迟一秒将audioPlayer 释放
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, .2f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
//        [self stopAudioPlayer];
//    });
}

#pragma mark - Notification
- (void)applicationWillResignActive:(UIApplication *)application {
    [self cancelOperation];
}

- (void)proximityStateChanged:(NSNotification *)notification {
    //如果此时手机靠近面部放在耳朵旁，那么声音将通过听筒输出，并将屏幕变暗，以达到省电的目的。
    if ([[UIDevice currentDevice] proximityState] == YES) {
        NSLog(@"Device is close to user");
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
    } else {
        NSLog(@"Device is not close to user");
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
    }
}

- (void)handleInterruption:(NSNotification *)notificaiton {
    NSLog(@"handleInterruption%@", notificaiton.userInfo);
}

#pragma mark - Getters
- (NSString *)cachePath {
    if (!_cachePath) {
        _cachePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"com.huihui.chat.audioCache"];
        if (![[NSFileManager defaultManager] fileExistsAtPath:_cachePath]) {
            [[NSFileManager defaultManager] createDirectoryAtPath:_cachePath withIntermediateDirectories:YES attributes:nil error:nil];
        }
    }
    return _cachePath;
}

- (NSTimeInterval)duration {
    return _audioPlayer.duration;
}

#pragma mark - Setters
- (void)setURLString:(NSString *)URLString {
    if (_URLString) {
        //说明当前有正在播放, 或者正在加载的视频,取消 operation(如果没有在执行任务),停止播放
        [self cancelOperation];
        [self stopAudioPlayer];
        [self setAudioPlayerState:HHAudioPlayState_Cancel];
    }
    _URLString = [URLString copy];
}

- (void)setAudioPlayerState:(HHAudioPlayState)audioPlayerState {
    _audioPlayerState = audioPlayerState;
    if (_audioPlayerState == HHAudioPlayState_Cancel ||
        _audioPlayerState == HHAudioPlayState_Normal ||
        _audioPlayerState == EHHAudioPlayState_End) {
        _URLString = nil;
        _identifier = nil;
    }
    if (audioPlayerState == HHAudioPlayState_Playing) {
        if (_audioTimer == nil) {
            _audioTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updatePlayerDuration) userInfo:nil repeats:YES];
            [_audioTimer fire];
        } else {
            [_audioTimer setFireDate:[NSDate distantPast]];
        }
    } else if (audioPlayerState == HHAudioPlayState_Pause) {
        [_audioTimer setFireDate:[NSDate distantFuture]]; //暂停计时器
    } else {
        [_audioTimer invalidate];
        _audioTimer = nil;
    }

    NSHashTable *bindDelegates = [self.bindedDelegates mutableCopy];
    for (id<HHAudioPlayerDelegate> bindItem in bindDelegates) {
        if ([bindItem respondsToSelector:@selector(playAudio:stateDidChanged:)]) {
            [bindItem playAudio:self stateDidChanged:audioPlayerState];
        }
    }
}

#pragma mark - help
- (void)obtainDurationWithURLString:(NSString *)URLString finished:(void (^)(float duration))finished {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSURL *soundUrl = nil;
        if ([URLString hasPrefix:@"http"] || [URLString hasPrefix:@"https"]) {
            soundUrl = [NSURL URLWithString:URLString];
        } else {
            soundUrl = [NSURL fileURLWithPath:URLString];
        }
        NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey: @(YES) };
        AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:soundUrl options:options];
        float seconds = CMTimeGetSeconds(asset.duration);

        dispatch_async(dispatch_get_main_queue(), ^{
            if (finished) {
                finished(seconds);
            }
        });
    });
}

- (HHAudioMediaType)mediaTypeWithUrl:(NSString *)URLString {
    NSString *pathExtension = [[URLString pathExtension] lowercaseString];
    if ([pathExtension hasSuffix:@"mp3"]) {
        return HHAudioMediaType_mp3;
    } else if ([pathExtension hasSuffix:@"caf"]) {
        return HHAudioMediaType_caf;
    } else if ([pathExtension hasSuffix:@"wav"]) {
        return HHAudioMediaType_wav;
    } else if ([pathExtension hasSuffix:@"amr"]) {
        return HHAudioMediaType_amr;
    } else {
        return HHAudioMediaType_unknow;
    }
}

- (BOOL)saveCache:(NSString *)localPath forUrl:(NSString *)URLString {
    NSString *audioCacheKey = [URLString bc_md5];
    NSData *audioData = [NSData dataWithContentsOfFile:localPath];
    if (!audioData) {
        return NO;
    }
    return [audioData writeToFile:[self.cachePath stringByAppendingPathComponent:audioCacheKey] atomically:YES];
}
@end
